None
Цель исследования: Выявить группы пользователей различающихся по паттерну поведения и предложить методы увеличения конверсии
Задачи:
Описание датасета: Датасет содержит данные о событиях, совершенных в мобильном приложении "Ненужные вещи". В нем пользователи продают свои ненужные вещи, размещая их на доске объявлений.В датасете содержатся данные пользователей, впервые совершивших действия в приложении после 7 октября 2019 года.
Колонки в mobile_sources.csv:
Колонки в mobile_dataset.csv:
Виды действий:
import pandas as pd
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime as dt
from datetime import timedelta
from tqdm import tqdm
import plotly
from plotly import graph_objects as go
import requests
from scipy import stats as st
import math as mth
import warnings
warnings.filterwarnings('ignore')
path = 'https://code.s3.yandex.net/datasets/'
df_sources = pd.read_csv(path + 'mobile_sources.csv')
df_base = pd.read_csv(path + 'mobile_dataset.csv')
df_sources.head()
| userId | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 |
df_base.head()
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
Приведем названия колонок к стандарту Python
df_sources.columns = ['user_id', 'source']
df_base.columns = ['event_time', 'event_name', 'user_id']
Проверим информацию о датасетах и кол-во пропусков
df_sources.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB
df_sources.isna().sum()
user_id 0 source 0 dtype: int64
В первом датафрейме пропусков нет. Всего 4293 записи
df_base.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 74197 non-null object 1 event_name 74197 non-null object 2 user_id 74197 non-null object dtypes: object(3) memory usage: 1.7+ MB
df_base.isna().sum()
event_time 0 event_name 0 user_id 0 dtype: int64
О чудо, во втором датафрейме пропусков так же нет! Всего 74197 записей
Приведем колонку со временм к типу datetime
df_base.event_time = pd.to_datetime(df_base.event_time)
df_base.duplicated().sum()
0
df_sources.duplicated().sum()
0
Явных полных дубликатов нет.
В колонке с типом события есть два варианта(contacts_show и show_contacts), означающие одно и то же действие событий, заменим их на одно название
df_base.event_name.unique()
array(['advert_open', 'tips_show', 'map', 'contacts_show', 'search_4',
'search_5', 'tips_click', 'photos_show', 'search_1', 'search_2',
'search_3', 'favorites_add', 'contacts_call', 'search_6',
'search_7', 'show_contacts'], dtype=object)
df_base.event_name = df_base.event_name.replace('show_contacts', 'contacts_show')
df_base.event_name.unique()
array(['advert_open', 'tips_show', 'map', 'contacts_show', 'search_4',
'search_5', 'tips_click', 'photos_show', 'search_1', 'search_2',
'search_3', 'favorites_add', 'contacts_call', 'search_6',
'search_7'], dtype=object)
df_sources.source.unique()
array(['other', 'yandex', 'google'], dtype=object)
Всего три источника
df_base.describe(datetime_is_numeric=True)
| event_time | |
|---|---|
| count | 74197 |
| mean | 2019-10-21 15:32:09.039316992 |
| min | 2019-10-07 00:00:00.431357 |
| 25% | 2019-10-14 22:04:27.791869952 |
| 50% | 2019-10-22 00:26:56.715014912 |
| 75% | 2019-10-28 12:35:53.023877120 |
| max | 2019-11-03 23:58:12.532487 |
Датасет содержит данные с 7 октября 2019 года по 3 ноября 2019 года. Всего в датасете 4293 уникальных пользователя
df_bar = df_base.copy()
df_bar['days'] = df_bar.event_time.dt.date
df_bar = df_bar.groupby('days', as_index=False).user_id.count()
fig, ax = plt.subplots(figsize=(8,8))
sns.barplot(data = df_bar, y='days', x='user_id', color='blue', saturation=0.3)
ax.set_title('Распределение количества событий по дням')
ax.set_xlabel('Количество событий')
ax.set_ylabel('Дата')
plt.show()
Распределение данных по датом в целом равномерное, с некоторым уменьшением событий в определенные даты, судя по всему, ближе к выходным активность снижается. Вновь увеличиваясь с началом недели
df_sort = df_base.sort_values(by=['user_id','event_time',]).reset_index(drop=True)
df_sort
| event_time | event_name | user_id | |
|---|---|---|---|
| 0 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 1 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 2 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 3 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 4 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| ... | ... | ... | ... |
| 74192 | 2019-11-03 15:51:23.959572 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74193 | 2019-11-03 15:51:57.899997 | contacts_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74194 | 2019-11-03 16:07:40.932077 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74195 | 2019-11-03 16:08:18.202734 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74196 | 2019-11-03 16:08:25.388712 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
74197 rows × 3 columns
Оценим разброс разницы во времени между событиями
list_procentiles = [i/100 for i in range(50, 100, 2)]
df_sort.groupby('user_id').event_time.diff().describe(list_procentiles)
count 69904 mean 0 days 03:53:01.169952121 std 1 days 01:32:57.259354294 min 0 days 00:00:00.000001 50% 0 days 00:01:10.535313 52% 0 days 00:01:15.529802920 54% 0 days 00:01:20.740343160 56% 0 days 00:01:26.509672 58.0% 0 days 00:01:32.373528179 60% 0 days 00:01:39.097805799 62% 0 days 00:01:46.413821060 64% 0 days 00:01:54.557998199 66% 0 days 00:02:02.940966140 68% 0 days 00:02:13.070328400 70% 0 days 00:02:24.286672400 72% 0 days 00:02:37.019077080 74% 0 days 00:02:51.758851980 76% 0 days 00:03:09.799350880 78% 0 days 00:03:33.563989220 80% 0 days 00:04:02.012437400 82% 0 days 00:04:40.479162500 84% 0 days 00:05:38.470597079 86% 0 days 00:07:07.229878720 88% 0 days 00:10:01.558655919 90% 0 days 00:17:00.807463700 92% 0 days 00:45:25.305054720 94% 0 days 03:31:32.660409639 96% 0 days 19:41:18.907537440 98% 2 days 04:26:53.696510520 max 26 days 13:49:54.853516 Name: event_time, dtype: object
В качестве таймаута выберем 15 минут. Большая часть действий(примерно 89%) относятся к одной сессии. Если пользователь отвлекается к примеру на поиск информации о товаре, 15 минут вполне для этого достаточно
Присваиваем id сессии каждому действию выбирая в качестве порога 15 минут
df_sort[df_sort.groupby('user_id').event_time.diff() < pd.Timedelta('15Min')]
| event_time | event_name | user_id | |
|---|---|---|---|
| 1 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 2 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 3 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 4 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| 5 | 2019-10-07 13:45:43.212340 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 |
| ... | ... | ... | ... |
| 74191 | 2019-11-03 15:50:56.073089 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74192 | 2019-11-03 15:51:23.959572 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74193 | 2019-11-03 15:51:57.899997 | contacts_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74195 | 2019-11-03 16:08:18.202734 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
| 74196 | 2019-11-03 16:08:25.388712 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b |
62660 rows × 3 columns
g = (df_sort.groupby('user_id').event_time.diff() > pd.Timedelta('15Min')).cumsum()
df_sort['session_id'] = df_sort.groupby(['user_id', g], sort=False).ngroup() + 1
df_sort
| event_time | event_name | user_id | session_id | |
|---|---|---|---|---|
| 0 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| 1 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| 2 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| 3 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| 4 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| ... | ... | ... | ... | ... |
| 74192 | 2019-11-03 15:51:23.959572 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 11536 |
| 74193 | 2019-11-03 15:51:57.899997 | contacts_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 11536 |
| 74194 | 2019-11-03 16:07:40.932077 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 11537 |
| 74195 | 2019-11-03 16:08:18.202734 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 11537 |
| 74196 | 2019-11-03 16:08:25.388712 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 11537 |
74197 rows × 4 columns
Удалим дубликаты событий в сессии, создав копию с дубликатами для расчета частоты событий
df_sort_unf = df_sort.copy()
df_sort_unf['step'] = df_sort_unf.groupby('session_id').cumcount() + 1
df_sort_unf['source'] = df_sort_unf['event_name']
df_sort_unf['target'] = df_sort_unf.groupby('session_id')['source'].shift(-1)
df_sort_unf.drop('event_name', axis=1, inplace=True)
df_sort.duplicated(subset=['event_name', 'session_id']).sum()
53541
df_sort = df_sort.drop_duplicates(subset=['event_name', 'session_id'], keep='first')
Удалено 53541 дубликата
df_sort.head(40)
| event_time | event_name | user_id | session_id | |
|---|---|---|---|---|
| 0 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 1 |
| 9 | 2019-10-09 18:33:55.577963 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2 |
| 11 | 2019-10-09 18:40:28.738785 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2 |
| 13 | 2019-10-21 19:52:30.778932 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3 |
| 15 | 2019-10-21 19:53:38.767230 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 3 |
| 27 | 2019-10-22 11:18:14.635436 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 4 |
| 28 | 2019-10-22 11:19:10.529462 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 4 |
| 35 | 2019-10-19 21:34:33.849769 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 5 |
| 38 | 2019-10-19 21:40:38.990477 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 5 |
| 44 | 2019-10-20 18:49:24.115634 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 6 |
| 45 | 2019-10-20 18:59:22.541082 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 6 |
| 46 | 2019-10-20 19:03:02.030004 | favorites_add | 00157779-810c-4498-9e05-a1e9e3cedf93 | 6 |
| 50 | 2019-10-20 19:17:18.659799 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 6 |
| 51 | 2019-10-20 19:17:24.887762 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 6 |
| 58 | 2019-10-20 19:57:15.652784 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 7 |
| 60 | 2019-10-20 20:04:53.349091 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 7 |
| 62 | 2019-10-24 10:50:40.219833 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 8 |
| 64 | 2019-10-24 10:52:18.644065 | advert_open | 00157779-810c-4498-9e05-a1e9e3cedf93 | 8 |
| 71 | 2019-10-29 21:18:24.850073 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 9 |
| 72 | 2019-10-29 21:19:35.389792 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 9 |
| 78 | 2019-10-29 21:26:40.258472 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 9 |
| 79 | 2019-10-29 21:26:51.574840 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 9 |
| 82 | 2019-10-29 21:53:34.469417 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 10 |
| 83 | 2019-10-29 21:53:40.073678 | contacts_call | 00157779-810c-4498-9e05-a1e9e3cedf93 | 10 |
| 84 | 2019-10-29 21:55:52.805158 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 10 |
| 88 | 2019-10-29 22:10:06.896644 | favorites_add | 00157779-810c-4498-9e05-a1e9e3cedf93 | 10 |
| 89 | 2019-10-29 22:10:21.561467 | advert_open | 00157779-810c-4498-9e05-a1e9e3cedf93 | 10 |
| 91 | 2019-10-30 07:50:45.948358 | search_1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 11 |
| 92 | 2019-10-30 07:53:12.730053 | photos_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 11 |
| 99 | 2019-10-30 08:01:05.420773 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 11 |
| 104 | 2019-10-30 08:26:53.933176 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 12 |
| 105 | 2019-11-03 17:12:09.708771 | contacts_show | 00157779-810c-4498-9e05-a1e9e3cedf93 | 13 |
| 106 | 2019-11-01 13:54:35.385028 | photos_show | 00463033-5717-4bf1-91b4-09183923b9df | 14 |
| 116 | 2019-10-18 22:14:05.555052 | search_7 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 15 |
| 117 | 2019-10-18 22:14:16.960831 | search_5 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 15 |
| 118 | 2019-10-18 22:17:40.719687 | map | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 15 |
| 119 | 2019-10-20 17:47:18.569612 | search_7 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 16 |
| 120 | 2019-10-20 17:47:19.889629 | search_4 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 16 |
| 121 | 2019-10-20 17:47:38.353167 | search_6 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 16 |
| 122 | 2019-10-20 17:47:42.437735 | search_5 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 16 |
Зададим новые колонки для диаграммы Санкей
df_sort['step'] = df_sort.groupby('session_id').cumcount() + 1
df_sort['source'] = df_sort['event_name']
df_sort['target'] = df_sort.groupby('session_id')['source'].shift(-1)
df_sort.drop('event_name', axis=1, inplace=True)
temp_ = df_sort[df_sort.step <= 3].reset_index(drop=True)
temp_ = temp_[~((temp_.step == 1) & (temp_.source == 'contacts_show'))]
list_id = temp_[temp_.source == 'contacts_show'].session_id.unique().tolist()
temp_ = df_sort[df_sort.session_id.isin(list_id)]
temp_ = temp_[temp_.step <= 3].reset_index(drop=True)
temp_ = temp_[temp_.session_id.isin(list_id)]
Зададим функцию для определения паттернов заканчивающихся целевым событием
def filtr(df):
"""
Функция для отбора действий пользователей заканчивающихся целевым событием
Args: df = база данных в формате pd.DataFrame
Return: база в формате pd.DataFrame сотфильтрованными данными
"""
for i in list_id:
indexes = df[df.session_id == i].index
temp = df[df.session_id == i].source.str.find('contacts_show')
find = temp[temp == 0].index[0]
if len(indexes) == 1:
continue
if df.loc[indexes[-1], 'source'] == 'contacts_show':
df = df.drop(list(range(find+1, indexes[-1])), axis=0)
else:
df = df.drop(list(range(find+1, indexes[-1]+1)), axis=0)
return df
df_filtred = filtr(temp_)
df_filtred.reset_index(drop=True, inplace=True)
Отберем те сессии где имееются целевые события
def get_source_index(df):
"""Функция генерации индексов source
Args:
df (pd.DataFrame): исходная таблица с признаками step, source, target.
Returns:
dict: словарь с индексами, именами и соответсвиями индексов именам source.
"""
res_dict = {}
count = 0
# получаем индексы источников
for no, step in enumerate(df['step'].unique().tolist()):
# получаем уникальные наименования для шага
res_dict[no+1] = {}
res_dict[no+1]['sources'] = df[df['step'] == step]['source'].unique().tolist()
res_dict[no+1]['sources_index'] = []
for i in range(len(res_dict[no+1]['sources'])):
res_dict[no+1]['sources_index'].append(count)
count += 1
# соединим списки
for key in res_dict:
res_dict[key]['sources_dict'] = {}
for name, no in zip(res_dict[key]['sources'], res_dict[key]['sources_index']):
res_dict[key]['sources_dict'][name] = no
return res_dict
source_indexes = get_source_index(df_filtred)
def colors_for_sources(mode, df):
"""Генерация цветов rgba
Args:
mode (str): сгенерировать случайные цвета, если 'random', а если 'custom' -
использовать заранее подготовленные
Returns:
dict: словарь с цветами, соответствующими каждому индексу
"""
# словарь, в который сложим цвета в соответствии с индексом
colors_dict = {}
if mode == 'random':
# генерим случайные цвета
for label in df['source'].unique():
r, g, b = np.random.randint(255, size=3)
colors_dict[label] = f'rgba({r}, {g}, {b}, 1)'
elif mode == 'custom':
# присваиваем ранее подготовленные цвета
colors = requests.get('https://raw.githubusercontent.com/rusantsovsv/senkey_tutorial/main/json/colors_senkey.json').json()
for no, label in enumerate(df['source'].unique()):
colors_dict[label] = colors['custom_colors'][no]
return colors_dict
colors_dict = colors_for_sources(mode='custom', df=df_filtred)
def percent_users(sources, targets, values):
"""
Расчет уникальных id в процентах (для вывода в hover text каждого узла)
Args:
sources (list): список с индексами source.
targets (list): список с индексами target.b
values (list): список с "объемами" потоков.
Returns:
list: список с "объемами" потоков в процентах
"""
# объединим источники и метки и найдем пары
zip_lists = list(zip(sources, targets, values))
new_list = []
# подготовим список словарь с общим объемом трафика в узлах
unique_dict = {}
# проходим по каждому узлу
for source, target, value in zip_lists:
if source not in unique_dict:
# находим все источники и считаем общий трафик
unique_dict[source] = 0
for sr, tg, vl in zip_lists:
if sr == source:
unique_dict[source] += vl
# считаем проценты
for source, target, value in zip_lists:
new_list.append(round(100 * value / unique_dict[source], 1))
return new_list
def lists_for_plot(df,source_indexes=source_indexes, colors=colors_dict, frac=10):
"""
Создаем необходимые для отрисовки диаграммы переменные списков и возвращаем
их в виде словаря
Args:
source_indexes (dict): словарь с именами и индексами source.
colors (dict): словарь с цветами source.
frac (int): ограничение на минимальный "объем" между узлами.
Returns:
dict: словарь со списками, необходимыми для диаграммы.
"""
sources = []
targets = []
values = []
labels = []
link_color = []
link_text = []
# проходим по каждому шагу
for step in tqdm(sorted(df['step'].unique()), desc='Шаг'):
if step + 1 not in source_indexes:
continue
# получаем индекс источника
temp_dict_source = source_indexes[step]['sources_dict']
# получаем индексы цели
temp_dict_target = source_indexes[step+1]['sources_dict']
# проходим по каждой возможной паре, считаем количество таких пар
for source, index_source in tqdm(temp_dict_source.items()):
for target, index_target in temp_dict_target.items():
# делаем срез данных и считаем количество id
temp_df = df[(df['step'] == step)&(df['source'] == source)&(df['target'] == target)]
value = len(temp_df)
# проверяем минимальный объем потока и добавляем нужные данные
if value > frac:
sources.append(index_source)
targets.append(index_target)
values.append(value)
# делаем поток прозрачным для лучшего отображения
link_color.append(colors[source].replace(', 1)', ', 0.4)'))
labels = []
colors_labels = []
for key in source_indexes:
for name in source_indexes[key]['sources']:
labels.append(name)
colors_labels.append(colors[name])
# посчитаем проценты всех потоков
perc_values = percent_users(sources, targets, values)
# добавим значения процентов для howertext
link_text = []
for perc in perc_values:
link_text.append(f"{perc}%")
# возвратим словарь с вложенными списками
return {'sources': sources,
'targets': targets,
'values': values,
'labels': labels,
'colors_labels': colors_labels,
'link_color': link_color,
'link_text': link_text}
data_for_plot = lists_for_plot(df=df_filtred)
Шаг: 0%| | 0/3 [00:00<?, ?it/s] 0%| | 0/13 [00:00<?, ?it/s] 100%|██████████| 13/13 [00:00<00:00, 94.79it/s] Шаг: 33%|███▎ | 1/3 [00:00<00:00, 7.14it/s] 100%|██████████| 10/10 [00:00<00:00, 915.29it/s] Шаг: 100%|██████████| 3/3 [00:00<00:00, 19.46it/s]
for i in data_for_plot['sources']:
if i not in data_for_plot['targets'] and i >= 14:
print(i)
for key, item in source_indexes[2]['sources_dict'].items():
if item == i:
data_for_plot['sources'] = [source_indexes[1]['sources_dict'][key] if x == i else x for x in data_for_plot['sources']]
print(key, item)
16 advert_open 16 20 favorites_add 20
def plot_senkey_diagram(data_dict=data_for_plot):
"""
Функция для генерации объекта диаграммы Сенкей
Args:
data_dict (dict): словарь со списками данных для построения.
Returns:
plotly.graph_objs._figure.Figure: объект изображения.
"""
fig = go.Figure(data=[go.Sankey(
domain = dict(
x = [0,1],
y = [0,1]
),
orientation = "h",
valueformat = ".0f",
node = dict(
pad = 50,
thickness = 15,
line = dict(color = "black", width = 0.1),
label = data_dict['labels'],
color = data_dict['colors_labels']
),
link = dict(
source = data_dict['sources'],
target = data_dict['targets'],
value = data_dict['values'],
label = data_dict['link_text'],
color = data_dict['link_color']
))])
fig.update_layout(title_text="Паттерны приводящие к целевому событию", font_size=10, width=1000, height=800)
# возвращаем объект диаграммы
return fig
# сохраняем диаграмму в переменную
senkey_diagram = plot_senkey_diagram()
senkey_diagram.show()
По диаграмме видно, что наибольшее количество целевых событий происходит в виде: показ рекомендации - просмотр контакта, поисковое действие 1 - просмотр контакта, показ фото - просмотр контакта, просмотр карты - показ рекомендации - просмотр контакта
funnel = df_filtred.groupby(['session_id']).source.apply(tuple).reset_index(name = 'events')
Определим основные паттерны действий и отберем те из них, количество действия по которым больше 10
funnel = funnel.groupby(['events'], as_index=False).session_id.count().sort_values('session_id', ascending=False).query("session_id > 10").rename({'session_id':'amount'}, axis=1)
funnel
| events | amount | |
|---|---|---|
| 33 | (tips_show, contacts_show) | 458 |
| 14 | (photos_show, contacts_show) | 137 |
| 18 | (search_1, contacts_show) | 125 |
| 13 | (map, tips_show, contacts_show) | 96 |
| 20 | (search_1, photos_show, contacts_show) | 54 |
| 10 | (map, contacts_show) | 38 |
| 35 | (tips_show, map, contacts_show) | 29 |
| 0 | (advert_open, contacts_show) | 20 |
| 37 | (tips_show, tips_click, contacts_show) | 20 |
| 3 | (advert_open, tips_show, contacts_show) | 12 |
| 16 | (photos_show, search_1, contacts_show) | 12 |
| 5 | (favorites_add, contacts_show) | 12 |
Чаще всего встречаются паттерны:
Построим основные воронки по уникальным пользователям
Создам сводную таблицу по которой буду определять наличией действий у пользователей
pivot = df_sort.pivot_table(index=['session_id', 'user_id'], columns = 'source', values = 'step', aggfunc='sum').reset_index()
success_1 = pivot[(pivot.tips_show >= 1) & (pivot.contacts_show >= 1)].user_id.nunique()
overall_1 = pivot[pivot.tips_show >= 1].user_id.nunique()
funnel_1 = pd.DataFrame(data={'step':['tips_show','contacts_show'],
'conversion':['{:.2%}'.format(1), '{:.2%}'.format(success_1 / overall_1)],
'users_count':[overall_1, success_1]})
funnel_1
| step | conversion | users_count | |
|---|---|---|---|
| 0 | tips_show | 100.00% | 2801 |
| 1 | contacts_show | 17.42% | 488 |
success_1 = pivot[(pivot.photos_show >= 1) & (pivot.contacts_show >= 1)].user_id.nunique()
overall_1 = pivot[pivot.photos_show >= 1].user_id.nunique()
funnel_2 = pd.DataFrame(data={'step':['photos_show','contacts_show'],
'conversion':['{:.2%}'.format(1), '{:.2%}'.format(success_1 / overall_1)],
'users_count':[overall_1, success_1]})
funnel_2
| step | conversion | users_count | |
|---|---|---|---|
| 0 | photos_show | 100.00% | 1095 |
| 1 | contacts_show | 22.92% | 251 |
success_1 = pivot[(pivot.search_1 >= 1) & (pivot.contacts_show >= 1)].user_id.nunique()
overall_1 = pivot[pivot.search_1 >= 1].user_id.nunique()
funnel_3 = pd.DataFrame(data={'step':['search_1','contacts_show'],
'conversion':['{:.2%}'.format(1),'{:.2%}'.format(success_1 / overall_1)],
'users_count':[overall_1, success_1]})
funnel_3
| step | conversion | users_count | |
|---|---|---|---|
| 0 | search_1 | 100.00% | 787 |
| 1 | contacts_show | 22.49% | 177 |
success_1 = pivot[(pivot.map >= 1) & (pivot.tips_show >= 1)].user_id.nunique()
success_2 = pivot[(pivot.tips_show >= 1) & (pivot.contacts_show >= 1)].user_id.nunique()
overall_1 = pivot[pivot.map >= 1].user_id.nunique()
funnel_4 = pd.DataFrame(data={'step':['map','tips_show','contacts_show', 'Общая конверсия'],
'conversion':['{:.2%}'.format(1), '{:.2%}'.format(success_1 / overall_1),
'{:.2%}'.format(success_2 / success_1),
'{:.2%}'.format(success_2/overall_1)],
'users_count':[overall_1, success_1, success_2,success_2]})
funnel_4
| step | conversion | users_count | |
|---|---|---|---|
| 0 | map | 100.00% | 1456 |
| 1 | tips_show | 88.46% | 1288 |
| 2 | contacts_show | 37.89% | 488 |
| 3 | Общая конверсия | 33.52% | 488 |
Как видно наибольшая конверсия у воронки 4: "просмот карты - показ рекомендации - просмотр контактов". Конверсия у воронки "показ рекомендации - показ контакта" из представленных воронок имеет наименьшую конверсию, что неудивительно, учитывая большую частоту показа рекомендаций, если улучшить качество и точность рекомендательной системы можно увеличить конверсию для большого числа пользователей.
Составим список с id сессий для групп с просмотром контактов и без
list_contacts_show = df_sort_unf[df_sort_unf.source == 'contacts_show'].session_id.unique().tolist()
last_time_event = (df_sort_unf[df_sort_unf.session_id.isin(list_contacts_show)]
.groupby('session_id', as_index=False)
.event_time.last())
first_time_event = (df_sort_unf[df_sort_unf.session_id.isin(list_contacts_show)]
.groupby('session_id', as_index=False)
.event_time.first())
time_dif_show = last_time_event - first_time_event
time_dif_show.drop('session_id', axis=1, inplace=True)
time_dif_show
| event_time | |
|---|---|
| 0 | 0 days 00:41:11.981283 |
| 1 | 0 days 00:08:20.581289 |
| 2 | 0 days 00:11:35.359427 |
| 3 | 0 days 00:16:51.243423 |
| 4 | 0 days 00:17:58.456492 |
| ... | ... |
| 1853 | 0 days 00:00:00.258294 |
| 1854 | 0 days 00:00:00 |
| 1855 | 0 days 00:04:56.677281 |
| 1856 | 0 days 00:15:48.307055 |
| 1857 | 0 days 00:15:56.892557 |
1858 rows × 1 columns
Уберем сессии с нулевой длительностью
time_dif_show = time_dif_show[~(time_dif_show.event_time == timedelta(0))]
time_dif_show.describe()
| event_time | |
|---|---|
| count | 1685 |
| mean | 0 days 00:14:52.507098158 |
| std | 0 days 00:18:00.958912221 |
| min | 0 days 00:00:00.000151 |
| 25% | 0 days 00:02:54.387005 |
| 50% | 0 days 00:09:16.645018 |
| 75% | 0 days 00:20:16.206534 |
| max | 0 days 03:12:23.263951 |
Среднее время сессии для сессий с просмотром контактов - 14 минут, 52 секунды. Медиана - 9 минуты 16 секунд
Сделаем то же самое для группы без просмотра контактов
time_dif_not_show = df_sort_unf[~df_sort_unf.session_id.isin(list_contacts_show)].groupby('session_id', as_index=False).event_time.last() - \
df_sort_unf[~df_sort_unf.session_id.isin(list_contacts_show)].groupby('session_id', as_index=False).event_time.first()
time_dif_not_show.drop('session_id', axis=1, inplace=True)
time_dif_not_show = time_dif_not_show[~(time_dif_not_show.event_time == timedelta(0))]
time_dif_not_show
| event_time | |
|---|---|
| 0 | 0 days 00:09:55.727258 |
| 1 | 0 days 00:08:27.385985 |
| 2 | 0 days 00:14:59.272096 |
| 3 | 0 days 00:12:38.171767 |
| 4 | 0 days 00:25:20.787329 |
| ... | ... |
| 9671 | 0 days 00:00:02.122046 |
| 9672 | 0 days 00:02:23.484632 |
| 9673 | 0 days 00:00:02.128781 |
| 9675 | 0 days 00:00:22.310348 |
| 9678 | 0 days 00:00:44.456635 |
7234 rows × 1 columns
time_dif_not_show.describe()
| event_time | |
|---|---|
| count | 7234 |
| mean | 0 days 00:11:35.505474576 |
| std | 0 days 00:14:12.020087420 |
| min | 0 days 00:00:00.000181 |
| 25% | 0 days 00:02:29.942116250 |
| 50% | 0 days 00:07:01.663663500 |
| 75% | 0 days 00:15:26.117887 |
| max | 0 days 03:53:38.911775 |
Среднее время сессии для сессий без просмотра контактов - 11 минут, 35 секунд. Медиана - 7 минуты 01 секунд. Среднее время и медиана для группы без просмотра контактов меньше чем в первой группе. Возможно это связано с отсутствием интересующего товара
Создадим список пользователей с просмотром контактов
list_users_contact = df_base[df_base.event_name == 'contacts_show'].user_id.unique()
Расчитаем относительную частоту событий в группе с просмотром контактов, убрав события просмотр контактов и звонок по номеру, что бы они не оттягивали на себя частоту событий. Нас больше интересует сравнение частот нецелевых событий
df_users = df_base.copy()
df_users = df_users[df_users.user_id.isin(list_users_contact)]
df_users = df_users[~df_users.event_name.isin(['contacts_show','contacts_call'])]
df_users_grouped = df_users.groupby('event_name', as_index=False).user_id.count()
df_users_grouped['rate'] = df_users_grouped.user_id / df_users_grouped.user_id.sum()
df_users_grouped = df_users_grouped.sort_values(by='rate', ascending = False).rename({'user_id':'amount_events'}, axis=1)
df_users_grouped.rate = df_users_grouped.rate.apply('{:.2%}'.format, axis=0)
df_users_grouped
| event_name | amount_events | rate | |
|---|---|---|---|
| 12 | tips_show | 12768 | 57.70% |
| 3 | photos_show | 3828 | 17.30% |
| 0 | advert_open | 1589 | 7.18% |
| 4 | search_1 | 1341 | 6.06% |
| 2 | map | 1101 | 4.98% |
| 1 | favorites_add | 424 | 1.92% |
| 11 | tips_click | 333 | 1.50% |
| 8 | search_5 | 249 | 1.13% |
| 7 | search_4 | 149 | 0.67% |
| 6 | search_3 | 144 | 0.65% |
| 5 | search_2 | 96 | 0.43% |
| 9 | search_6 | 74 | 0.33% |
| 10 | search_7 | 31 | 0.14% |
Расчитаем относительную частоту событий в группе без просмотра контактов
df_users = df_base.copy()
df_users = df_users[~df_users.user_id.isin(list_users_contact)]
df_users_grouped = df_users.groupby('event_name', as_index=False).user_id.count()
df_users_grouped['rate'] = df_users_grouped.user_id / df_users_grouped.user_id.sum()
df_users_grouped = df_users_grouped.sort_values(by='rate', ascending = False).rename({'user_id':'amount_events'}, axis=1)
df_users_grouped.rate = df_users_grouped.rate.apply('{:.2%}'.format, axis=0)
df_users_grouped
| event_name | amount_events | rate | |
|---|---|---|---|
| 12 | tips_show | 27287 | 58.06% |
| 3 | photos_show | 6184 | 13.16% |
| 0 | advert_open | 4575 | 9.73% |
| 2 | map | 2780 | 5.91% |
| 4 | search_1 | 2165 | 4.61% |
| 1 | favorites_add | 993 | 2.11% |
| 8 | search_5 | 800 | 1.70% |
| 7 | search_4 | 552 | 1.17% |
| 11 | tips_click | 481 | 1.02% |
| 9 | search_6 | 386 | 0.82% |
| 6 | search_3 | 378 | 0.80% |
| 5 | search_2 | 228 | 0.49% |
| 10 | search_7 | 191 | 0.41% |
У группы с просмотром контактов чаще всего встречается показ рекомендаций, затем показ фото, затем открытие объявления, на четвертом месте поисковое действие 1
Чаще всего у группы без показа контактов встречается показ рекомендации, на втором месте показ фото, на третьем месте открытие объявления, на четвертом открытие карты
В сравнении двух групп: группы с просмотром контактов реже встречается показ рекомендаций(57.70% против 58.06%), открытие объявления(7.18% против 9.73%), просмотр карты (4.98% против 5.91%). При этом частота следующих событий выше: показ фото(17.30% против 13.16%), поисковое действие 1 (6.06% против 4.61%)
Возможно большая частота поискового события 1 связана с поиском определенного товара и изначальной нацеленностью на покупку. Так же можно предположить, что показ фото положительно влияет на конверсию. При этом частота события открытие объявления ниже чем в группе без целевого действия, похоже люди нацеленные на покупку не склонны к долгому просмотру объявлений.
Создадим сводную таблицу
df_base
| event_time | event_name | user_id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| ... | ... | ... | ... |
| 74192 | 2019-11-03 23:53:29.534986 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 |
| 74193 | 2019-11-03 23:54:00.407086 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 |
| 74194 | 2019-11-03 23:56:57.041825 | search_1 | 20850c8f-4135-4059-b13b-198d3ac59902 |
| 74195 | 2019-11-03 23:57:06.232189 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 |
| 74196 | 2019-11-03 23:58:12.532487 | tips_show | 28fccdf4-7b9e-42f5-bc73-439a265f20e9 |
74197 rows × 3 columns
df_base.pivot_table(index=['user_id'], columns = 'event_name', values = 'event_time', aggfunc='count').reset_index()
| event_name | user_id | advert_open | contacts_call | contacts_show | favorites_add | map | photos_show | search_1 | search_2 | search_3 | search_4 | search_5 | search_6 | search_7 | tips_click | tips_show |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | NaN | NaN | NaN | NaN | 6.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 29.0 |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2.0 | 5.0 | 11.0 | 2.0 | NaN | 33.0 | 18.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2 | 00463033-5717-4bf1-91b4-09183923b9df | NaN | NaN | NaN | NaN | NaN | 10.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 5.0 | NaN | NaN | NaN | 6.0 | NaN | NaN | NaN | 1.0 | 2.0 | 6.0 | 2.0 | 6.0 | NaN | 4.0 |
| 4 | 00551e79-152e-4441-9cf7-565d7eb04090 | NaN | 3.0 | 3.0 | NaN | NaN | 1.0 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 4288 | ffab8d8a-30bb-424a-a3ab-0b63ebbf7b07 | NaN | NaN | NaN | NaN | 2.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 15.0 |
| 4289 | ffc01466-fdb1-4460-ae94-e800f52eb136 | NaN | NaN | 1.0 | NaN | NaN | 6.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 4290 | ffcf50d9-293c-4254-8243-4890b030b238 | NaN | NaN | NaN | NaN | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1.0 |
| 4291 | ffe68f10-e48e-470e-be9b-eeb93128ff1a | NaN | NaN | 1.0 | NaN | NaN | 7.0 | 5.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 4292 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | NaN | NaN | 68.0 | NaN | 2.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 233.0 |
4293 rows × 16 columns
pivot = df_base.pivot_table(index=['user_id'], columns = 'event_name', values = 'event_time', aggfunc='count').reset_index().fillna(0).drop('user_id', axis=1)
Оценим корреляцию
pivot
| event_name | advert_open | contacts_call | contacts_show | favorites_add | map | photos_show | search_1 | search_2 | search_3 | search_4 | search_5 | search_6 | search_7 | tips_click | tips_show |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.0 | 0.0 | 0.0 | 0.0 | 6.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 29.0 |
| 1 | 2.0 | 5.0 | 11.0 | 2.0 | 0.0 | 33.0 | 18.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 | 5.0 | 0.0 | 0.0 | 0.0 | 6.0 | 0.0 | 0.0 | 0.0 | 1.0 | 2.0 | 6.0 | 2.0 | 6.0 | 0.0 | 4.0 |
| 4 | 0.0 | 3.0 | 3.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 4288 | 0.0 | 0.0 | 0.0 | 0.0 | 2.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 15.0 |
| 4289 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 6.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4290 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 4291 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 7.0 | 5.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4292 | 0.0 | 0.0 | 68.0 | 0.0 | 2.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 233.0 |
4293 rows × 15 columns
pivot.corr()['contacts_show']
event_name advert_open 0.052408 contacts_call 0.205595 contacts_show 1.000000 favorites_add 0.015627 map 0.317998 photos_show 0.025482 search_1 0.039820 search_2 0.001502 search_3 0.048719 search_4 0.004140 search_5 0.073236 search_6 0.015025 search_7 0.018673 tips_click 0.214662 tips_show 0.424281 Name: contacts_show, dtype: float64
list_labels = ['открытие объявления', 'звонок по номеру', 'показ контакта',
'добавление в избранное', 'просмотр карты', 'просмотр фото', 'поисковое действие 1', 'поисковое действие 2',
'поисковое действие 3', 'поисковое действие 4', 'поисковое действие 5', 'поисковое действие 6',
'поисковое действие 7', 'клик по рекомендации', 'показ рекомендации']
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(pivot.corr())
ax.set_title("Корреляция между различными событиями")
ax.set_xlabel('Событие')
ax.set_ylabel('Событие')
ax.set_xticklabels(list_labels)
ax.set_yticklabels(list_labels)
plt.show()
Имеется умеренная корреляция между просмотром контакта и показом рекомендации, что впрочем может быть вызвано высокой частотой показа рекомендации пользователям. Так же имеется слабая положительная корреляция между кликом по рекомендации и просмотром контакта, а так же просмотром карты и просмотром контакта
Выделим две группы
Сначала создадим списки пользователей для группы
tmp = df_base[df_base.event_name.isin(['tips_show', 'tips_click'])]. \
pivot_table(index='user_id', columns='event_name', values=['event_time'], aggfunc='count') \
.reset_index().droplevel(0, axis=1).rename({'':'user_id'}, axis=1)
user_both = tmp.query("tips_click > 0 and tips_show > 0").user_id.unique()
user_tips_show = tmp.query("tips_show > 0 and tips_click.isna()").user_id.unique()
Расчитаем конверсию
temp_1 = df_base[(df_base.user_id.isin(user_both))]
target_count_1 = temp_1[temp_1.event_name == 'contacts_show'].user_id.nunique()
overall_1 = temp_1.user_id.nunique()
print("Показатель конверсии для группы совершившей оба действия")
print(f"Кол-во пользователей совершивших целевое действие: {target_count_1}")
print(f"Общее кол-во пользователей: {overall_1}")
print('Показатель конверсии: ', end = '')
print('{:.2%}'.format(target_count_1 / overall_1))
Показатель конверсии для группы совершившей оба действия Кол-во пользователей совершивших целевое действие: 91 Общее кол-во пользователей: 297 Показатель конверсии: 30.64%
temp_2 = df_base[(df_base.user_id.isin(user_tips_show))]
target_count_2 = temp_2[temp_2.event_name == 'contacts_show'].user_id.nunique()
overall_2 = temp_2.user_id.nunique()
print("Показатель конверсии для группы совершившей оба действия")
print(f"Кол-во пользователей совершивших целевое действие: {target_count_2}")
print(f"Общее кол-во пользователей: {overall_2}")
print('Показатель конверсии: ', end = '')
print('{:.2%}'.format(target_count_2 / overall_2))
Показатель конверсии для группы совершившей оба действия Кол-во пользователей совершивших целевое действие: 425 Общее кол-во пользователей: 2504 Показатель конверсии: 16.97%
Проверим статистическую гипотезу:
Гипотеза 1: Конверсия у пользователей совершающих действия tips_show и tips_click отличается от пользователей совершаюших только действие tips_show.
Нулевая гипотеза: Конверсия у пользователей двух указанных групп не различается
Альтернативная гипотеза: Конверсия у пользователей двух указанных групп различается
Уровень значимости: а=0.05 (является стандартным уровнем, в нашей ситуации нет поводов уменьшать или увеличивать этот уровень)
Проверим гипотезу с помощью Z критерия
p1 = target_count_1 / overall_1
p2 = target_count_2 / overall_2
p_combined = (target_count_1 + target_count_2) / (overall_1 + overall_2)
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/overall_1 + 1/overall_2))
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
alpha = 0.05
if p_value < alpha:
print(f'p-value = {p_value}, что позволяет отвергнуть нулевую гипотезу')
else:
print(f'p-value = {p_value}, что не позволяет отвергнуть нулевую гипотезу')
p-value = 9.218316554537864e-09, что позволяет отвергнуть нулевую гипотезу
Конверсия для первой группы равна: 30.64%, Конверсия для второй группы равна: 16.97%. Те пользователи которые просмотрели рекоментации и кликнули на них имеют большую конверсию в просмотр контакта. Разница статистически значима с p-value = 9.21e-09
Зададим две группы, отличающиеся по наличию действия "добавить в избранное"
list_favorite = df_base[df_base.event_name == 'favorites_add'].user_id.unique()
temp_1 = df_base[df_base.user_id.isin(list_favorite)]
target_count_1 = temp_1[temp_1.event_name == 'contacts_show'].user_id.nunique()
overall_1 = temp_1.user_id.nunique()
print("Показатель конверсии для группы совершившей оба действия")
print(f"Кол-во пользователей совершивших целевое действие: {target_count_1}")
print(f"Общее кол-во пользователей: {overall_1}")
print('Показатель конверсии: ', end = '')
print('{:.2%}'.format(target_count_1 / overall_1))
Показатель конверсии для группы совершившей оба действия Кол-во пользователей совершивших целевое действие: 136 Общее кол-во пользователей: 351 Показатель конверсии: 38.75%
temp_2 = df_base[~df_base.user_id.isin(list_favorite)]
target_count_2 = temp_2[temp_2.event_name == 'contacts_show'].user_id.nunique()
overall_2 = temp_2.user_id.nunique()
print("Показатель конверсии для группы совершившей оба действия")
print(f"Кол-во пользователей совершивших целевое действие: {target_count_2}")
print(f"Общее кол-во пользователей: {overall_2}")
print('Показатель конверсии: ', end = '')
print('{:.2%}'.format(target_count_2 / overall_2))
Показатель конверсии для группы совершившей оба действия Кол-во пользователей совершивших целевое действие: 845 Общее кол-во пользователей: 3942 Показатель конверсии: 21.44%
Проверим статистическую гипотезу:
Гипотеза 2: Пользователи добавившие объявление в избранное чаще делают больше одного целевого действия чем пользователи не совершающие действие "добавить в избранное"
Нулевая гипотеза: Пользователи двух указанных групп имеют одинаковую долю совершающих и не совершающих повторно целевое действие
Альтернативная гипотеза: Пользователи двух указанных групп имеют разную долю совершающих и не совершающих повторно целевое действие
Уровень значимости: а=0.05 (является стандартным уровнем, в нашей ситуации нет поводов уменьшать или увеличивать этот уровень)
Проверим гипотезу с помощью критерия Манна-Уитни(он невосприимчив к аномалиям, выбросам, так как не является параметрическим)
p1 = target_count_1 / overall_1
p2 = target_count_2 / overall_2
p_combined = (target_count_1 + target_count_2) / (overall_1 + overall_2)
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/overall_1 + 1/overall_2))
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
alpha = 0.05
if p_value < alpha:
print(f'p-value = {p_value}, что позволяет отвергнуть нулевую гипотезу')
else:
print(f'p-value = {p_value}, что не позволяет отвергнуть нулевую гипотезу')
p-value = 1.3455903058456897e-13, что позволяет отвергнуть нулевую гипотезу
Конверсия для первой группы равна: 38.75%, Конверсия для второй группы равна: 21.44%. Те пользователи которые совершили действие "добавить в избранное" имеют большую конверсию, чем те, которые это действие не совершали. Разница статистически значима с p-value = 1.35e-13.
Сравним относительную частоту событий для группы с добавленным в избранное и без
temp_1 = df_base[(df_base.user_id.isin(list_favorite))&(df_base.event_name !='favorites_add')]
temp = temp_1.groupby('event_name', as_index=False).user_id.nunique().rename({'user_id':'amount'}, axis=1)
temp['rate'] = (temp.amount / temp.amount.sum())
temp.sort_values(by='rate', ascending=False, inplace=True)
temp.rate = temp.rate.apply('{:.2%}'.format, axis=0)
temp
| event_name | amount | rate | |
|---|---|---|---|
| 4 | photos_show | 202 | 20.68% |
| 5 | search_1 | 161 | 16.48% |
| 2 | contacts_show | 136 | 13.92% |
| 0 | advert_open | 120 | 12.28% |
| 13 | tips_show | 108 | 11.05% |
| 3 | map | 64 | 6.55% |
| 1 | contacts_call | 41 | 4.20% |
| 9 | search_5 | 36 | 3.68% |
| 12 | tips_click | 32 | 3.28% |
| 8 | search_4 | 23 | 2.35% |
| 6 | search_2 | 19 | 1.94% |
| 10 | search_6 | 14 | 1.43% |
| 7 | search_3 | 13 | 1.33% |
| 11 | search_7 | 8 | 0.82% |
temp_2 = df_base[~df_base.user_id.isin(list_favorite)]
temp = temp_2.groupby('event_name', as_index=False).user_id.count().rename({'user_id':'amount'}, axis=1)
temp['rate'] = (temp.amount / temp.amount.sum())
temp.sort_values(by='rate', ascending=False, inplace=True)
temp.rate = temp.rate.apply('{:.2%}'.format, axis=0)
temp
| event_name | amount | rate | |
|---|---|---|---|
| 13 | tips_show | 35695 | 57.18% |
| 4 | photos_show | 7354 | 11.78% |
| 0 | advert_open | 5213 | 8.35% |
| 2 | contacts_show | 3746 | 6.00% |
| 3 | map | 3485 | 5.58% |
| 5 | search_1 | 2687 | 4.30% |
| 9 | search_5 | 999 | 1.60% |
| 12 | tips_click | 716 | 1.15% |
| 8 | search_4 | 664 | 1.06% |
| 7 | search_3 | 473 | 0.76% |
| 1 | contacts_call | 448 | 0.72% |
| 10 | search_6 | 444 | 0.71% |
| 6 | search_2 | 296 | 0.47% |
| 11 | search_7 | 209 | 0.33% |
Группа с событием добавить в избранное имеет совсем другой состав частот событий. Они гораздо чаще совершают поисковое событие 1, событие - "показ фото", "открытие объявления", при этом гораздо меньше события "показ рекомендации". Возможно пользователи добавляющие в избранное ищут что-то конкретное, так как совершают поисковое событие 1 и потому имеют большую конверсию. Возможно стоит активнее предлагать пользователям добавлять товар в избранное, чтобы увеличить конверсию. Правда если добавление в избранное - лишь следствие определенного мотива пользователя(покупка конкретного товара), это не окажет существенного влияния на конверсию
temp_ = df_sort_unf[df_sort_unf.source == 'contacts_show'].groupby('user_id', as_index=False).source.count()
list_users = temp_[temp_.source > 1].user_id.unique()
list_users_1 = temp_[temp_.source == 1].user_id.unique()
temp_1 = df_sort_unf[df_sort_unf.user_id.isin(list_users)]
temp_1 = temp_1.drop_duplicates(subset=['user_id', 'source'])
temp = temp_1.groupby('source', as_index=False).user_id.count().rename({'user_id':'amount'}, axis=1)
temp['rate'] = (temp.amount / temp_1.user_id.nunique())
temp.sort_values(by='rate', ascending=False, inplace=True)
temp.rate = temp.rate.apply('{:.2%}'.format, axis=0)
temp
| source | amount | rate | |
|---|---|---|---|
| 2 | contacts_show | 601 | 100.00% |
| 14 | tips_show | 312 | 51.91% |
| 5 | photos_show | 195 | 32.45% |
| 4 | map | 182 | 30.28% |
| 1 | contacts_call | 169 | 28.12% |
| 6 | search_1 | 139 | 23.13% |
| 0 | advert_open | 92 | 15.31% |
| 3 | favorites_add | 86 | 14.31% |
| 10 | search_5 | 73 | 12.15% |
| 13 | tips_click | 67 | 11.15% |
| 9 | search_4 | 60 | 9.98% |
| 7 | search_2 | 38 | 6.32% |
| 11 | search_6 | 37 | 6.16% |
| 8 | search_3 | 29 | 4.83% |
| 12 | search_7 | 15 | 2.50% |
temp_1 = df_sort_unf[df_sort_unf.user_id.isin(list_users_1)]
temp_1 = temp_1.drop_duplicates(subset=['user_id', 'source'])
temp = temp_1.groupby('source', as_index=False).user_id.count().rename({'user_id':'amount'}, axis=1)
temp['rate'] = (temp.amount / temp_1.user_id.nunique())
temp.sort_values(by='rate', ascending=False, inplace=True)
temp.rate = temp.rate.apply('{:.2%}'.format, axis=0)
temp
| source | amount | rate | |
|---|---|---|---|
| 2 | contacts_show | 380 | 100.00% |
| 14 | tips_show | 204 | 53.68% |
| 5 | photos_show | 144 | 37.89% |
| 4 | map | 107 | 28.16% |
| 6 | search_1 | 98 | 25.79% |
| 3 | favorites_add | 50 | 13.16% |
| 0 | advert_open | 46 | 12.11% |
| 1 | contacts_call | 44 | 11.58% |
| 10 | search_5 | 41 | 10.79% |
| 13 | tips_click | 33 | 8.68% |
| 9 | search_4 | 28 | 7.37% |
| 7 | search_2 | 17 | 4.47% |
| 11 | search_6 | 15 | 3.95% |
| 12 | search_7 | 10 | 2.63% |
| 8 | search_3 | 9 | 2.37% |
В целом распределение частот в группах с более чем одним целевым действием мало отличается от группы c одним целевым действием, разве что в первой группе больше звонков по номеру, что не удивительно, учитывая большее кол-во просмотренных контактов.